Skip to content

fix(upgrade): install a missing target instead of running its updater - #112

Merged
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/upgrade-install-missing-target
Jul 31, 2026
Merged

fix(upgrade): install a missing target instead of running its updater#112
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/upgrade-install-missing-target

Conversation

@clawedassistant26

Copy link
Copy Markdown
Contributor

The bug

moshcode upgrade <target> cannot install a target that has a native updater. It announces that it is installing, then runs the binary that is missing:

$ moshcode upgrade doppler        # doppler not on PATH
⬆ upgrading doppler (installing — not present) — doppler update
✗ doppler upgrade failed (spawn doppler ENOENT)

✗ upgraded 0/1 — failed: doppler 🤘

Why

planUpgrade computes an installed flag and then ignores it one line later:

spec: toolUpgradeSpec(TOOLS[key]),
installed: toolByKey[key].installed,

toolUpgradeSpec/upgradeSpec return entry.upgrade || entry.install, so any entry with a native updater plans that updater even when the target is absent. That contract is documented as installed-only, in src/engines.mjs:

The command that upgrades an already-installed engine in place

Affected: tools doppler (doppler update) and tailscale (tailscale update); engines opencode, privacycode (<bin> upgrade) and aider (aider --upgrade). Every other entry has no upgrade field, so the || entry.install fallback masks it.

This contradicts what the code says it does in three places:

  • src/upgrade.mjs doc comment: ["claude"|"ugig", …] → named targets (install if not present yet)
  • src/upgrade.mjs: // Explicit names/engine aliases upgrade even when not currently installed.
  • the printed note itself: (installing — not present)

It also contradicts the existing test, which is named "explicit tool upgrades use official installers even when not installed" — but only exercises ugig and coinpay, the two tools with no upgrade field, so it passes while the claim is false.

Repro

On unmodified main (1c05460), with doppler not on PATH, the real CLI output above. At the planning layer:

doppler      installed=false  spec=doppler ["update"]
opencode     installed=false  spec=opencode ["upgrade"]
aider        installed=false  spec=aider ["--upgrade"]
ugig         installed=false  spec=bash ["-c","curl -fsSL https://ugig.net/install.sh | bash"]   ← correct, no native updater

After the fix moshcode upgrade doppler installs it for real (Installed Doppler CLI v3.76.1), and tailscale, which is installed here, still plans tailscale update.

The fix

Choose the spec from the flag that was already being computed: installer when absent, native updater when present. 11 insertions / 4 deletions, 5 of the additions comment.

Tests

New test/upgrade-install-missing.test.mjs, 11 tests. 5 are the bug, 6 are controls that pass both before and after.

The bug tests cover a missing tool, a missing engine, a missing engine reached through an alias (pc → privacycode), plus a sweep over every entry that has a native updater — derived from ENGINES/TOOLS rather than hardcoded, so a newly added updater is covered automatically instead of silently skipped.

The controls deliberately assert the other direction so the fix cannot buy installability by throwing away the native updaters: an installed tool and an installed engine still plan doppler update / opencode upgrade; entries with no native updater are unchanged either way; tools/engines/all still only include installed targets; unknown targets are still collected rather than planned.

Fail-before via git checkout -- src/upgrade.mjs: 5 fail / 6 pass unpatched, 11/11 patched. Full suite 391 → 402, 0 failures.

Deliberately not included

  • test/upgrade.test.mjs's existing test keeps its misleading name and its ugig/coinpay targets. Renaming or widening it is yours to call; the new file covers the gap without touching it.
  • src/cli.mjs --dry-run returns { ok: true, dryRun: true } with no code, contradicting its own JSDoc (Returns { ok, code } — always). Separate defect, happy to send it if useful.

`moshcode upgrade doppler` on a box without doppler printed
"(installing — not present)" and then ran `doppler update` — the very
binary that is missing — so it failed with ENOENT instead of installing.

planUpgrade computed an `installed` flag and then ignored it, calling
upgradeSpec/toolUpgradeSpec unconditionally. Those helpers prefer an
entry's native updater, which is only valid for something already on
disk (engines.mjs: "The command that upgrades an already-installed
engine in place").

Affected every entry that has a native updater: tools doppler and
tailscale, engines opencode, privacycode and aider. Entries without one
fall back to `install` on their own, which is why this stayed hidden.

Fix: use the target's installer when it is not present, keep the native
updater when it is.
@ralyodio
ralyodio merged commit fd418d7 into moshcoder:main Jul 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants